/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.commons.configuration; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.Set; import junit.framework.Assert; import junit.framework.TestCase; import org.apache.commons.configuration.event.ConfigurationEvent; import org.apache.commons.configuration.event.ConfigurationListener; import org.apache.commons.configuration.reloading.FileAlwaysReloadingStrategy; import org.apache.commons.configuration.tree.DefaultExpressionEngine; import org.apache.commons.configuration.tree.NodeCombiner; import org.apache.commons.configuration.tree.UnionCombiner; /** * Test class for CombinedConfiguration. * * @version $Id: TestCombinedConfiguration.java 712401 2008-11-08 15:29:56Z oheger $ */ public class TestCombinedConfiguration extends TestCase { /** Constant for the name of a sub configuration. */ private static final String TEST_NAME = "SUBCONFIG"; /** Constant for a test key. */ private static final String TEST_KEY = "test.value"; /** Constant for the name of the first child configuration.*/ private static final String CHILD1 = TEST_NAME + "1"; /** Constant for the name of the second child configuration.*/ private static final String CHILD2 = TEST_NAME + "2"; /** Constant for the name of the XML reload test file.*/ private static final String RELOAD_XML_NAME = "reload.xml"; /** Constant for the content of a XML reload test file.*/ private static final String RELOAD_XML_CONTENT = "<xml><xmlReload>{0}</xmlReload></xml>"; /** Constant for the name of the properties reload test file.*/ private static final String RELOAD_PROPS_NAME = "reload.properties"; /** Constant for the content of a properties reload test file.*/ private static final String RELOAD_PROPS_CONTENT = "propsReload = {0}"; /** Constant for the directory for writing test files.*/ private static final File TEST_DIR = new File("target"); /** A list with files created during a test.*/ private Collection testFiles; /** The configuration to be tested. */ private CombinedConfiguration config; /** The test event listener. */ private CombinedListener listener; protected void setUp() throws Exception { super.setUp(); config = new CombinedConfiguration(); listener = new CombinedListener(); config.addConfigurationListener(listener); } /** * Performs clean-up after a test run. If test files have been created, they * are removed now. */ protected void tearDown() throws Exception { if (testFiles != null) { for (Iterator it = testFiles.iterator(); it.hasNext();) { File f = (File) it.next(); if (f.exists()) { assertTrue("Cannot delete test file: " + f, f.delete()); } } } } /** * Tests accessing a newly created combined configuration. */ public void testInit() { assertEquals("Already configurations contained", 0, config .getNumberOfConfigurations()); assertTrue("Set of names is not empty", config.getConfigurationNames() .isEmpty()); assertTrue("Wrong node combiner", config.getNodeCombiner() instanceof UnionCombiner); assertNull("Test config was found", config.getConfiguration(TEST_NAME)); assertFalse("Force reload check flag is set", config.isForceReloadCheck()); } /** * Tests adding a configuration (without further information). */ public void testAddConfiguration() { AbstractConfiguration c = setUpTestConfiguration(); config.addConfiguration(c); checkAddConfig(c); assertEquals("Wrong number of configs", 1, config .getNumberOfConfigurations()); assertTrue("Name list is not empty", config.getConfigurationNames() .isEmpty()); assertSame("Added config not found", c, config.getConfiguration(0)); assertTrue("Wrong property value", config.getBoolean(TEST_KEY)); listener.checkEvent(1, 0); } /** * Tests adding a configuration with a name. */ public void testAddConfigurationWithName() { AbstractConfiguration c = setUpTestConfiguration(); config.addConfiguration(c, TEST_NAME); checkAddConfig(c); assertEquals("Wrong number of configs", 1, config .getNumberOfConfigurations()); assertSame("Added config not found", c, config.getConfiguration(0)); assertSame("Added config not found by name", c, config .getConfiguration(TEST_NAME)); Set names = config.getConfigurationNames(); assertEquals("Wrong number of config names", 1, names.size()); assertTrue("Name not found", names.contains(TEST_NAME)); assertTrue("Wrong property value", config.getBoolean(TEST_KEY)); listener.checkEvent(1, 0); } /** * Tests adding a configuration with a name when this name already exists. * This should cause an exception. */ public void testAddConfigurationWithNameTwice() { config.addConfiguration(setUpTestConfiguration(), TEST_NAME); try { config.addConfiguration(setUpTestConfiguration(), TEST_NAME, "prefix"); fail("Could add config with same name!"); } catch (ConfigurationRuntimeException cex) { // ok } } /** * Tests adding a configuration and specifying an at position. */ public void testAddConfigurationAt() { AbstractConfiguration c = setUpTestConfiguration(); config.addConfiguration(c, null, "my"); checkAddConfig(c); assertTrue("Wrong property value", config.getBoolean("my." + TEST_KEY)); } /** * Tests adding a configuration with a complex at position. Here the at path * contains a dot, which must be escaped. */ public void testAddConfigurationComplexAt() { AbstractConfiguration c = setUpTestConfiguration(); config.addConfiguration(c, null, "This..is.a.complex"); checkAddConfig(c); assertTrue("Wrong property value", config .getBoolean("This..is.a.complex." + TEST_KEY)); } /** * Checks if a configuration was correctly added to the combined config. * * @param c the config to check */ private void checkAddConfig(AbstractConfiguration c) { Collection listeners = c.getConfigurationListeners(); assertEquals("Wrong number of configuration listeners", 1, listeners .size()); assertTrue("Combined config is no listener", listeners.contains(config)); } /** * Tests adding a null configuration. This should cause an exception to be * thrown. */ public void testAddNullConfiguration() { try { config.addConfiguration(null); fail("Could add null configuration!"); } catch (IllegalArgumentException iex) { // ok } } /** * Tests accessing properties if no configurations have been added. */ public void testAccessPropertyEmpty() { assertFalse("Found a key", config.containsKey(TEST_KEY)); assertNull("Key has a value", config.getString("test.comment")); assertTrue("Config is not empty", config.isEmpty()); } /** * Tests accessing properties if multiple configurations have been added. */ public void testAccessPropertyMulti() { config.addConfiguration(setUpTestConfiguration()); config.addConfiguration(setUpTestConfiguration(), null, "prefix1"); config.addConfiguration(setUpTestConfiguration(), null, "prefix2"); assertTrue("Prop1 not found", config.getBoolean(TEST_KEY)); assertTrue("Prop 2 not found", config.getBoolean("prefix1." + TEST_KEY)); assertTrue("Prop 3 not found", config.getBoolean("prefix2." + TEST_KEY)); assertFalse("Configuration is empty", config.isEmpty()); listener.checkEvent(3, 0); } /** * Tests removing a configuration. */ public void testRemoveConfiguration() { AbstractConfiguration c = setUpTestConfiguration(); config.addConfiguration(c); checkAddConfig(c); assertTrue("Config could not be removed", config.removeConfiguration(c)); checkRemoveConfig(c); } /** * Tests removing a configuration by index. */ public void testRemoveConfigurationAt() { AbstractConfiguration c = setUpTestConfiguration(); config.addConfiguration(c); assertSame("Wrong config removed", c, config.removeConfigurationAt(0)); checkRemoveConfig(c); } /** * Tests removing a configuration by name. */ public void testRemoveConfigurationByName() { AbstractConfiguration c = setUpTestConfiguration(); config.addConfiguration(c, TEST_NAME); assertSame("Wrong config removed", c, config .removeConfiguration(TEST_NAME)); checkRemoveConfig(c); } /** * Tests removing a configuration with a name. */ public void testRemoveNamedConfiguration() { AbstractConfiguration c = setUpTestConfiguration(); config.addConfiguration(c, TEST_NAME); config.removeConfiguration(c); checkRemoveConfig(c); } /** * Tests removing a named configuration by index. */ public void testRemoveNamedConfigurationAt() { AbstractConfiguration c = setUpTestConfiguration(); config.addConfiguration(c, TEST_NAME); assertSame("Wrong config removed", c, config.removeConfigurationAt(0)); checkRemoveConfig(c); } /** * Tests removing a configuration that was not added prior. */ public void testRemoveNonContainedConfiguration() { assertFalse("Could remove non contained config", config .removeConfiguration(setUpTestConfiguration())); listener.checkEvent(0, 0); } /** * Tests removing a configuration by name, which is not contained. */ public void testRemoveConfigurationByUnknownName() { assertNull("Could remove configuration by unknown name", config .removeConfiguration("unknownName")); listener.checkEvent(0, 0); } /** * Tests whether a configuration was completely removed. * * @param c the removed configuration */ private void checkRemoveConfig(AbstractConfiguration c) { assertTrue("Listener was not removed", c.getConfigurationListeners() .isEmpty()); assertEquals("Wrong number of contained configs", 0, config .getNumberOfConfigurations()); assertTrue("Name was not removed", config.getConfigurationNames() .isEmpty()); listener.checkEvent(2, 0); } /** * Tests if an update of a contained configuration leeds to an invalidation * of the combined configuration. */ public void testUpdateContainedConfiguration() { AbstractConfiguration c = setUpTestConfiguration(); config.addConfiguration(c); c.addProperty("test.otherTest", "yes"); assertEquals("New property not found", "yes", config .getString("test.otherTest")); listener.checkEvent(2, 0); } /** * Tests if setting a node combiner causes an invalidation. */ public void testSetNodeCombiner() { NodeCombiner combiner = new UnionCombiner(); config.setNodeCombiner(combiner); assertSame("Node combiner was not set", combiner, config .getNodeCombiner()); listener.checkEvent(1, 0); } /** * Tests setting a null node combiner. This should cause an exception. */ public void testSetNullNodeCombiner() { try { config.setNodeCombiner(null); fail("Could set null node combiner!"); } catch (IllegalArgumentException iex) { // ok } } /** * Tests cloning a combined configuration. */ public void testClone() { config.addConfiguration(setUpTestConfiguration()); config.addConfiguration(setUpTestConfiguration(), TEST_NAME, "conf2"); config.addConfiguration(new PropertiesConfiguration(), "props"); CombinedConfiguration cc2 = (CombinedConfiguration) config.clone(); assertEquals("Wrong number of contained configurations", config .getNumberOfConfigurations(), cc2.getNumberOfConfigurations()); assertSame("Wrong node combiner", config.getNodeCombiner(), cc2 .getNodeCombiner()); assertEquals("Wrong number of names", config.getConfigurationNames() .size(), cc2.getConfigurationNames().size()); assertTrue("Event listeners were cloned", cc2 .getConfigurationListeners().isEmpty()); StrictConfigurationComparator comp = new StrictConfigurationComparator(); for (int i = 0; i < config.getNumberOfConfigurations(); i++) { assertNotSame("Configuration at " + i + " was not cloned", config .getConfiguration(i), cc2.getConfiguration(i)); assertEquals("Wrong config class at " + i, config.getConfiguration( i).getClass(), cc2.getConfiguration(i).getClass()); assertTrue("Configs not equal at " + i, comp.compare(config .getConfiguration(i), cc2.getConfiguration(i))); } assertTrue("Combined configs not equal", comp.compare(config, cc2)); } /** * Tests if the cloned configuration is decoupled from the original. */ public void testCloneModify() { config.addConfiguration(setUpTestConfiguration(), TEST_NAME); CombinedConfiguration cc2 = (CombinedConfiguration) config.clone(); assertTrue("Name is missing", cc2.getConfigurationNames().contains( TEST_NAME)); cc2.removeConfiguration(TEST_NAME); assertFalse("Names in original changed", config.getConfigurationNames() .isEmpty()); } /** * Tests clearing a combined configuration. This should remove all contained * configurations. */ public void testClear() { config.addConfiguration(setUpTestConfiguration(), TEST_NAME, "test"); config.addConfiguration(setUpTestConfiguration()); config.clear(); assertEquals("Still configs contained", 0, config .getNumberOfConfigurations()); assertTrue("Still names contained", config.getConfigurationNames() .isEmpty()); assertTrue("Config is not empty", config.isEmpty()); listener.checkEvent(3, 2); } /** * Tests if file-based configurations can be reloaded. */ public void testReloading() throws Exception { config.setForceReloadCheck(true); File testXmlFile = writeReloadFile(RELOAD_XML_NAME, RELOAD_XML_CONTENT, 0); File testPropsFile = writeReloadFile(RELOAD_PROPS_NAME, RELOAD_PROPS_CONTENT, 0); XMLConfiguration c1 = new XMLConfiguration(testXmlFile); c1.setReloadingStrategy(new FileAlwaysReloadingStrategy()); PropertiesConfiguration c2 = new PropertiesConfiguration(testPropsFile); c2.setThrowExceptionOnMissing(true); c2.setReloadingStrategy(new FileAlwaysReloadingStrategy()); config.addConfiguration(c1); config.addConfiguration(c2); assertEquals("Wrong xml reload value", 0, config.getInt("xmlReload")); assertEquals("Wrong props reload value", 0, config .getInt("propsReload")); writeReloadFile(RELOAD_XML_NAME, RELOAD_XML_CONTENT, 1); assertEquals("XML reload not detected", 1, config.getInt("xmlReload")); config.setForceReloadCheck(false); writeReloadFile(RELOAD_PROPS_NAME, RELOAD_PROPS_CONTENT, 1); assertEquals("Props reload detected though check flag is false", 0, config .getInt("propsReload")); } /** * Tests whether the reload check works with a subnode configuration. This * test is related to CONFIGURATION-341. */ public void testReloadingSubnodeConfig() throws IOException, ConfigurationException { config.setForceReloadCheck(true); File testXmlFile = writeReloadFile(RELOAD_XML_NAME, RELOAD_XML_CONTENT, 0); XMLConfiguration c1 = new XMLConfiguration(testXmlFile); c1.setReloadingStrategy(new FileAlwaysReloadingStrategy()); final String prefix = "reloadCheck"; config.addConfiguration(c1, CHILD1, prefix); SubnodeConfiguration sub = config.configurationAt(prefix, true); writeReloadFile(RELOAD_XML_NAME, RELOAD_XML_CONTENT, 1); assertEquals("Reload not detected", 1, sub.getInt("xmlReload")); } /** * Prepares a test of the getSource() method. */ private void setUpSourceTest() { HierarchicalConfiguration c1 = new HierarchicalConfiguration(); PropertiesConfiguration c2 = new PropertiesConfiguration(); c1.addProperty(TEST_KEY, TEST_NAME); c2.addProperty("another.key", "test"); config.addConfiguration(c1, CHILD1); config.addConfiguration(c2, CHILD2); } /** * Tests the gestSource() method when the source property is defined in a * hierarchical configuration. */ public void testGetSourceHierarchical() { setUpSourceTest(); assertEquals("Wrong source configuration", config .getConfiguration(CHILD1), config.getSource(TEST_KEY)); } /** * Tests whether the source configuration can be detected for non * hierarchical configurations. */ public void testGetSourceNonHierarchical() { setUpSourceTest(); assertEquals("Wrong source configuration", config .getConfiguration(CHILD2), config.getSource("another.key")); } /** * Tests the getSource() method when the passed in key is not contained. * Result should be null in this case. */ public void testGetSourceUnknown() { setUpSourceTest(); assertNull("Wrong result for unknown key", config .getSource("an.unknown.key")); } /** * Tests the getSource() method when a null key is passed in. This should * cause an exception. */ public void testGetSourceNull() { try { config.getSource(null); fail("Could resolve source for null key!"); } catch (IllegalArgumentException iex) { // ok } } /** * Tests the getSource() method when the passed in key belongs to the * combined configuration itself. */ public void testGetSourceCombined() { setUpSourceTest(); final String key = "yet.another.key"; config.addProperty(key, Boolean.TRUE); assertEquals("Wrong source for key", config, config.getSource(key)); } /** * Tests the getSource() method when the passed in key refers to multiple * values, which are all defined in the same source configuration. */ public void testGetSourceMulti() { setUpSourceTest(); final String key = "list.key"; config.getConfiguration(CHILD1).addProperty(key, "1,2,3"); assertEquals("Wrong source for multi-value property", config .getConfiguration(CHILD1), config.getSource(key)); } /** * Tests the getSource() method when the passed in key refers to multiple * values defined by different sources. This should cause an exception. */ public void testGetSourceMultiSources() { setUpSourceTest(); final String key = "list.key"; config.getConfiguration(CHILD1).addProperty(key, "1,2,3"); config.getConfiguration(CHILD2).addProperty(key, "a,b,c"); try { config.getSource(key); fail("Multiple sources not detected!"); } catch (IllegalArgumentException iex) { //ok } } /** * Tests whether escaped list delimiters are treated correctly. */ public void testEscapeListDelimiters() { PropertiesConfiguration sub = new PropertiesConfiguration(); sub.addProperty("test.pi", "3\\,1415"); config.addConfiguration(sub); assertEquals("Wrong value", "3,1415", config.getString("test.pi")); } /** * Tests whether an invalidate event is fired only after a change. This test * is related to CONFIGURATION-315. */ public void testInvalidateAfterChange() { ConfigurationEvent event = new ConfigurationEvent(config, 0, null, null, true); config.configurationChanged(event); assertEquals("Invalidate event fired", 0, listener.invalidateEvents); event = new ConfigurationEvent(config, 0, null, null, false); config.configurationChanged(event); assertEquals("No invalidate event fired", 1, listener.invalidateEvents); } /** * Tests using a conversion expression engine for child configurations with * strange keys. This test is related to CONFIGURATION-336. */ public void testConversionExpressionEngine() { PropertiesConfiguration child = new PropertiesConfiguration(); child.addProperty("test(a)", "1,2,3"); config.addConfiguration(child); DefaultExpressionEngine engineQuery = new DefaultExpressionEngine(); engineQuery.setIndexStart("<"); engineQuery.setIndexEnd(">"); config.setExpressionEngine(engineQuery); DefaultExpressionEngine engineConvert = new DefaultExpressionEngine(); engineConvert.setIndexStart("["); engineConvert.setIndexEnd("]"); config.setConversionExpressionEngine(engineConvert); assertEquals("Wrong property 1", "1", config.getString("test(a)<0>")); assertEquals("Wrong property 2", "2", config.getString("test(a)<1>")); assertEquals("Wrong property 3", "3", config.getString("test(a)<2>")); } /** * Tests whether reload operations can cause a deadlock when the combined * configuration is accessed concurrently. This test is related to * CONFIGURATION-344. */ public void testDeadlockWithReload() throws ConfigurationException, InterruptedException { final PropertiesConfiguration child = new PropertiesConfiguration( "test.properties"); child.setReloadingStrategy(new FileAlwaysReloadingStrategy()); config.addConfiguration(child); final int count = 1000; class ReloadThread extends Thread { boolean error = false; public void run() { for (int i = 0; i < count && !error; i++) { try { if (!child.getBoolean("configuration.loaded")) { error = true; } } catch (NoSuchElementException nsex) { error = true; } } } } ReloadThread reloadThread = new ReloadThread(); reloadThread.start(); for (int i = 0; i < count; i++) { assertEquals("Wrong value of combined property", 10, config .getInt("test.integer")); } reloadThread.join(); assertFalse("Failure in thread", reloadThread.error); } /** * Helper method for writing a file. The file is also added to a list and * will be deleted in teadDown() automatically. * * @param file the file to be written * @param content the file's content * @throws IOException if an error occurs */ private void writeFile(File file, String content) throws IOException { PrintWriter out = null; try { out = new PrintWriter(new FileWriter(file)); out.print(content); if (testFiles == null) { testFiles = new ArrayList(); } testFiles.add(file); } finally { if (out != null) { out.close(); } } } /** * Helper method for writing a test file. The file will be created in the * test directory. It is also scheduled for automatic deletion after the * test. * * @param fileName the name of the test file * @param content the content of the file * @return the <code>File</code> object for the test file * @throws IOException if an error occurs */ private File writeFile(String fileName, String content) throws IOException { File file = new File(TEST_DIR, fileName); writeFile(file, content); return file; } /** * Writes a file for testing reload operations. * * @param name the name of the reload test file * @param content the content of the file * @param value the value of the reload test property * @return the file that was written * @throws IOException if an error occurs */ private File writeReloadFile(String name, String content, int value) throws IOException { return writeFile(name, MessageFormat.format(content, new Object[] { new Integer(value) })); } /** * Helper method for creating a test configuration to be added to the * combined configuration. * * @return the test configuration */ private AbstractConfiguration setUpTestConfiguration() { HierarchicalConfiguration config = new HierarchicalConfiguration(); config.addProperty(TEST_KEY, Boolean.TRUE); config.addProperty("test.comment", "This is a test"); return config; } /** * Test event listener class for checking if the expected invalidate events * are fired. */ static class CombinedListener implements ConfigurationListener { int invalidateEvents; int otherEvents; public void configurationChanged(ConfigurationEvent event) { if (event.getType() == CombinedConfiguration.EVENT_COMBINED_INVALIDATE) { invalidateEvents++; } else { otherEvents++; } } /** * Checks if the expected number of events was fired. * * @param expectedInvalidate the expected number of invalidate events * @param expectedOthers the expected number of other events */ public void checkEvent(int expectedInvalidate, int expectedOthers) { Assert.assertEquals("Wrong number of invalidate events", expectedInvalidate, invalidateEvents); Assert.assertEquals("Wrong number of other events", expectedOthers, otherEvents); } } }